/*  PLUTO JS -Scripting Web Server.
    Copyright (C) 2013 vivek gangadharan

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package pluto.http;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpCookie;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Level;
import static pluto.core.AppLogger.*;
import pluto.core.Localizer;
import pluto.core.Message;

public class HTTPRequest {

    private final InputStream is;
    private String requestLine;
    private String method;
    private String uri;
    private boolean isAvalibleForRead;
    private HashMap<String, String> headers;
    private HashMap<String, String> parameters;
    private HashMap<String, File> files;
    private ByteArrayOutputStream f = new ByteArrayOutputStream();
    private int bytesRead = 0;
    private long size = 0x7FFFFFFFFFFFFFFFl;
    private byte[] bodyContent;
    private boolean isBadRequest = false;
    private boolean isInternalErrorDuringParsing = false;

    public HTTPRequest(InputStream is) throws Exception {
        this.is = is;
        this.isAvalibleForRead = true;
        headers = new HashMap();
        parameters = new HashMap();
        files = new HashMap();
        parseHeader();
    }

    private void parseHeader() throws Exception {
        try {
            if (is == null) {
                isInternalErrorDuringParsing = true;
                log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorClientSocketInputStreamNull));
                throw new NullPointerException(Localizer.getMessage(Message.ErrorClientSocketInputStreamNull));
            }

            final int bufsize = 8192;
            byte[] buf = new byte[bufsize];
            int splitbyte = 0;
            {
                int read = is.read(buf, 0, bufsize);
                while (read > 0) {
                    bytesRead += read;
                    splitbyte = findHeaderEnd(buf, bytesRead);
                    if (splitbyte > 0) {
                        break;
                    }
                    read = is.read(buf, bytesRead, bufsize - bytesRead);
                }
            }
            ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, bytesRead);
            BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
            decodeHeader(hin);
            if (splitbyte < bytesRead) {
                f.write(buf, splitbyte, bytesRead - splitbyte);
            }

            String contentLength = headers.get("content-length");
            if (contentLength != null) {
                try {
                    size = Integer.parseInt(contentLength);
                } catch (NumberFormatException ex) {
                }
            }
            if (splitbyte < bytesRead) {
                size -= bytesRead - splitbyte + 1;
            } else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) {
                size = 0;
            }
        } catch (Exception e) {
            isInternalErrorDuringParsing = true;
            isAvalibleForRead = false;
            log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorParsingHTTPHeader));
            throw e;
        }

    }

    private int findHeaderEnd(final byte[] buf, int rlen) {
        int splitbyte = 0;
        while (splitbyte + 3 < rlen) {
            if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
                return splitbyte + 4;
            }
            splitbyte++;
        }
        return 0;
    }

    private void decodeHeader(BufferedReader in) throws Exception {

        // Read the request line
        requestLine = in.readLine();
        if (requestLine == null) {
            return;
        }
        StringTokenizer st = new StringTokenizer(requestLine);
        if (!st.hasMoreTokens()) {
            isBadRequest = true;
            log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadRequest));
            throw new Exception(Localizer.getMessage(Message.HTTPBadRequest));
        }

        method = st.nextToken();


        if (!st.hasMoreTokens()) {
            isBadRequest = true;
            log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadRequest));
            throw new Exception(Localizer.getMessage(Message.HTTPBadRequest));
        }

        this.uri = st.nextToken();

        int qmi = uri.indexOf('?');
        if (qmi >= 0) {
            decodeParms(uri.substring(qmi + 1));
            uri = decodePercent(uri.substring(0, qmi));
        } else {
            uri = decodePercent(uri);
        }

        if (st.hasMoreTokens()) {
            String line = in.readLine();
            while (line != null && line.trim().length() > 0) {
                int p = line.indexOf(':');
                if (p >= 0) {
                    headers.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
                }
                line = in.readLine();
            }
        }

    }

    private String decodePercent(String str) throws Exception {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            switch (c) {
                case '+':
                    sb.append(' ');
                    break;
                case '%':
                    sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16));
                    i += 2;
                    break;
                default:
                    sb.append(c);
                    break;
            }
        }
        return sb.toString();
    }

    private void decodeParms(String parms) throws Exception {
        if (parms == null) {
            return;
        }
        StringTokenizer st = new StringTokenizer(parms, "&");
        while (st.hasMoreTokens()) {
            String e = st.nextToken();
            int sep = e.indexOf('=');
            if (sep >= 0) {
                parameters.put(decodePercent(e.substring(0, sep)).trim(),
                        decodePercent(e.substring(sep + 1)));
            }
        }
    }

    private void readBodyToStream() throws IOException {
        byte[] buf = new byte[512];
        while (bytesRead >= 0 && size > 0) {
            bytesRead = is.read(buf, 0, 512);
            size -= bytesRead;
            if (bytesRead > 0) {
                f.write(buf, 0, bytesRead);
            }
        }
        bodyContent = f.toByteArray();

    }

    private void parseBody() throws Exception {
        try {
            if (!isAvalibleForRead) {
                return;
            }
            readBodyToStream();
            ByteArrayInputStream bodyContentStream = new ByteArrayInputStream(bodyContent);
            BufferedReader in = new BufferedReader(new InputStreamReader(bodyContentStream));

            if (method != null && method.equalsIgnoreCase("POST")) {
                String contentType = "";
                String contentTypeHeader = headers.get("content-type");
                StringTokenizer st = new StringTokenizer(contentTypeHeader, "; ");
                if (st.hasMoreTokens()) {
                    contentType = st.nextToken();
                }

                if (contentType.equalsIgnoreCase("multipart/form-data")) {
                    // Handle multipart/form-data
                    if (!st.hasMoreTokens()) {
                        isBadRequest = true;
                        log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                        throw new Exception(Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                    }
                    String boundaryExp = st.nextToken();
                    st = new StringTokenizer(boundaryExp, "=");
                    if (st.countTokens() != 2) {
                        isBadRequest = true;
                        log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                        throw new Exception(Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                    }
                    st.nextToken();
                    String boundary = st.nextToken();

                    decodeMultipartData(boundary, bodyContent, in);
                } else {
                    // Handle application/x-www-form-urlencoded
                    String postLine = "";
                    char pbuf[] = new char[512];
                    int read = in.read(pbuf);
                    while (read >= 0 && !postLine.endsWith("\r\n")) {
                        postLine += String.valueOf(pbuf, 0, read);
                        read = in.read(pbuf);
                    }
                    postLine = postLine.trim();
                    decodeParms(postLine);
                }
            }

            if (method.equalsIgnoreCase("PUT")) {
                saveTmpFile("content", bodyContent, 0, f.size());
            }

        } catch (Exception e) {
            isInternalErrorDuringParsing = true;
            isAvalibleForRead = false;
            throw e;
        } finally {
            isAvalibleForRead = false;
        }
    }

    private int[] getBoundaryPositions(byte[] b, byte[] boundary) {
        int matchcount = 0;
        int matchbyte = -1;
        Vector matchbytes = new Vector();
        for (int i = 0; i < b.length; i++) {
            if (b[i] == boundary[matchcount]) {
                if (matchcount == 0) {
                    matchbyte = i;
                }
                matchcount++;
                if (matchcount == boundary.length) {
                    matchbytes.addElement(new Integer(matchbyte));
                    matchcount = 0;
                    matchbyte = -1;
                }
            } else {
                i -= matchcount;
                matchcount = 0;
                matchbyte = -1;
            }
        }
        int[] ret = new int[matchbytes.size()];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = ((Integer) matchbytes.elementAt(i)).intValue();
        }
        return ret;
    }

    private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in) throws Exception {

        int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
        int boundarycount = 1;
        String mpline = in.readLine();
        while (mpline != null) {
            if (mpline.indexOf(boundary) == -1) {
                isBadRequest = true;
                log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadMultiPartRequest2));
                throw new Exception(Localizer.getMessage(Message.HTTPBadMultiPartRequest2));
            }
            boundarycount++;
            Properties item = new Properties();
            mpline = in.readLine();
            while (mpline != null && mpline.trim().length() > 0) {
                int p = mpline.indexOf(':');
                if (p != -1) {
                    item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim());
                }
                mpline = in.readLine();
            }
            if (mpline != null) {
                String contentDisposition = item.getProperty("content-disposition");
                if (contentDisposition == null) {
                    isBadRequest = true;
                    log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadMultiPartRequest3));
                    throw new Exception(Localizer.getMessage(Message.HTTPBadMultiPartRequest3));

                }
                StringTokenizer st = new StringTokenizer(contentDisposition, "; ");
                Properties disposition = new Properties();
                while (st.hasMoreTokens()) {
                    String token = st.nextToken();
                    int p = token.indexOf('=');
                    if (p != -1) {
                        disposition.put(token.substring(0, p).trim().toLowerCase(), token.substring(p + 1).trim());
                    }
                }
                String pname = disposition.getProperty("name");
                pname = pname.substring(1, pname.length() - 1);

                String value = "";
                if (item.getProperty("content-type") == null) {
                    while (mpline != null && mpline.indexOf(boundary) == -1) {
                        mpline = in.readLine();
                        if (mpline != null) {
                            int d = mpline.indexOf(boundary);
                            if (d == -1) {
                                value += mpline;
                            } else {
                                value += mpline.substring(0, d - 2);
                            }
                        }
                    }
                } else {
                    if (boundarycount > bpositions.length) {
                        isBadRequest = true;
                        log.log(Level.SEVERE, Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                        throw new Exception(Localizer.getMessage(Message.HTTPBadMultiPartRequest));
                    }
                    int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
                    saveTmpFile(pname, fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
                    value = disposition.getProperty("filename");
                    value = value.substring(1, value.length() - 1);
                    do {
                        mpline = in.readLine();
                    } while (mpline != null && mpline.indexOf(boundary) == -1);
                }
                parameters.put(pname, value);
            }
        }

    }

    private int stripMultipartHeaders(byte[] b, int offset) {
        int i = 0;
        for (i = offset; i < b.length; i++) {
            if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n') {
                break;
            }
        }
        return i + 1;
    }

    private void saveTmpFile(String parameter, byte[] b, int offset, int len) throws Exception {
        if (len > 0) {
            String tmpdir = System.getProperty("java.io.tmpdir");
            File temp = File.createTempFile("Pluto", "", new File(tmpdir));
            OutputStream fstream = new FileOutputStream(temp);
            fstream.write(b, offset, len);
            fstream.close();

            files.put(parameter, temp);
        }
    }

    public void clean() throws Exception {
        try {
            is.close();
            f.close();
        } catch (Exception ex) {
            throw ex;
        }
        headers.clear();
        parameters.clear();
        files.clear();
        bodyContent = null;
    }

    public String header(String name) {
        return headers.get(name);
    }

    public Object[] headers() {
        if (headers.keySet() != null) {
            return headers.keySet().toArray();
        } else {
            return null;
        }
    }

    public String param(String name) throws Exception {
        if (isAvalibleForRead) {
            parseBody();
        }
        return parameters.get(name);
    }

    public Object[] params() throws Exception {
        if (isAvalibleForRead) {
            parseBody();
        }
        if (parameters.keySet() != null) {
            return parameters.keySet().toArray();
        } else {
            return null;
        }
    }

    public File file(String param) throws Exception {
        if (isAvalibleForRead) {
            parseBody();
        }
        return files.get(param);
    }

    public Object[] files() throws Exception {
        if (isAvalibleForRead) {
            parseBody();
        }
        if (files.keySet() != null) {
            return files.keySet().toArray();
        } else {
            return null;
        }
    }

    public String requestLine() {
        return requestLine;
    }

    public String uri() {
        return uri;
    }

    public String method() {
        return method;
    }

    public int headerBytes() {
        return bytesRead;
    }

    public InputStream inputStream() {
        return is;
    }

    public boolean isErrorParsing() {
        return isInternalErrorDuringParsing;
    }

    public boolean isBadRequest() {
        return isBadRequest;
    }

    public byte[] content() throws Exception {
        parseBody();
        return bodyContent;
    }

    public String string() throws Exception {
        parseBody();
        return new String(bodyContent);
    }

    public HttpCookie[] cookies() {
        if (headers.containsKey("Cookie")) {
            List<HttpCookie> cookies = HttpCookie.parse(headers.get("Cookie"));
            if (cookies != null) {
                return (HttpCookie[]) cookies.toArray();
            }
        }
        return null;
    }
}
